// Copyright (c) 2010 SuccessFactors, Inc.
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions
// are met:
//
//     * Redistributions of source code must retain the above
//       copyright notice, this list of conditions and the following
//       disclaimer.
//
//     * Redistributions in binary form must reproduce the above
//       copyright notice, this list of conditions and the following
//       disclaimer in the documentation and/or other materials
//       provided with the distribution.
//
//     * Neither the name of the SuccessFactors, Inc. nor the names of
//       its contributors may be used to endorse or promote products
//       derived from this software without specific prior written
//       permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
// FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
// COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
// INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
// HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
// OF THE POSSIBILITY OF SUCH DAMAGE.

package org.owasp.jxt;

import java.io.IOException;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
import org.xml.sax.Attributes;
import org.xml.sax.EntityResolver;
import org.xml.sax.InputSource;
import org.xml.sax.Locator;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;
import org.xml.sax.XMLReader;
import org.xml.sax.helpers.DefaultHandler;

/**
 * JxtParser
 *
 * @author Jeffrey Ichnowski
 * @version $Revision: 8 $
 */
final class JxtParser {

    static final SAXParserFactory SAX_FACTORY = SAXParserFactory.newInstance();
    static {
        SAX_FACTORY.setNamespaceAware(true);
        SAX_FACTORY.setValidating(false);

        // TODO: use XInclude from parser if available (currently
        // provided by a filter)
        // SAX_FACTORY.setXIncludeAware(true);
    }

    private JxtEngine _engine;
    private ValidationContext _validationContext;

    private JxtNode _current = new JxtNode("*root*") {};

    private JxtParser(JxtEngine engine, ValidationContext vctx) {
        _engine = engine;
        _validationContext = vctx;
    }

    private DefaultHandler createHandler() {
        return new DefaultHandler() {
            private Locator _locator;
            private StringBuilder _buffer = new StringBuilder();

            private <T extends JxtNode> T locate(T node) {
                if (_locator != null) {
                    node.setLocation(_locator);
                }
                return node;
            }

            private TextNode createText(String text) {
                // TODO: consolodate this replaceAll call, and also
                // make handle other "\\" escapes (e.g. "\\$", "\\\$",
                // etc...)
                return locate(new TextNode(text.replaceAll("\\\\\\$", "\\$")));
            }

            private void flushBuffer() {
                if (_buffer.length() > 0) {
                    new ExprParser() {
                        {
                            // We ignore lone $ in content.  The
                            // attribute ExprParser does not.  The
                            // reason being that "$" is much more
                            // likely to show up in content than it is
                            // in an attribute.
                            setIgnoreLoneDollar(true);
                        }
                        protected void text(String text) {
                            _current.appendChild(createText(text));
                        }
                        protected void expr(String expr) {
                            _current.appendChild(locate(new ExprNode(expr)));
                        }
                        @Override
                        protected void warn(String msg, Object ... args) {
                            _validationContext.warn(_current, msg, args);
                        }
                    }.parse(_buffer);
                    _buffer.setLength(0);
                }
            }

            @Override
            public void setDocumentLocator(Locator locator) {
                _locator = locator;
            }

            @Override
            public void startPrefixMapping(String prefix, String uri) throws SAXException {
                // TODO: try to load file?
                // System.out.println("startPrefixMapping("+prefix+", "+uri+")");
            }

            @Override
            public void characters(char[] ch, int start, int length)
                throws SAXException
            {
                _buffer.append(ch, start, length);
            }

            @Override
            public void startElement(String uri, String localName, String qName, Attributes attrs)
                throws SAXException
            {
                flushBuffer();

                JxtNodeFactory factory = _engine.getFactoryForNS(uri);

                if (factory == null) {
                    throw new SAXParseException("Invalid namespace '"+uri+"'", _locator);
                }

                JxtNode node;

                try {
                    node = factory.createNode(localName);
                } catch (IllegalArgumentException e) {
                    node = new ErrorNode(uri, localName);
                }

                _current.appendChild(locate(node));

                for (int i=0, n=attrs.getLength() ; i<n ; ++i) {
                    try {
                        node.setAttribute(_validationContext,
                                          attrs.getLocalName(i),
                                          attrs.getValue(i));
                    } catch (IllegalArgumentException e) {
                        _validationContext.error(node, e);
                    } catch (UnsupportedOperationException e) {
                        throw new SAXParseException(
                            "Error setting attribute "+attrs.getLocalName(i),
                            _locator, e);
                    }
                }

                _current = node;
            }

            @Override
            public void endElement(String uri, String localName, String qName)
                throws SAXException
            {
                flushBuffer();
                _current = _current.getParent();
            }
        };
    }

    public static JxtNode parse(JxtEngine engine, EntityResolver er, InputSource in, ValidationContext vctx)
        throws ParserConfigurationException, SAXException, IOException
    {
        return new JxtParser(engine, vctx).parse(er, in);
    }

    private JxtNode parse(EntityResolver er, InputSource in)
        throws ParserConfigurationException, SAXException, IOException
    {
        SAXParser parser = SAX_FACTORY.newSAXParser();
        XMLReader reader = new XIncludeFilter(SAX_FACTORY, parser.getXMLReader());
        DefaultHandler handler = createHandler();

        reader.setContentHandler(handler);
        reader.setEntityResolver(new DTDEntityResolver(er));
        reader.setErrorHandler(handler);
        reader.setDTDHandler(handler);

        reader.parse(in);

        JxtNode root = _current.getFirstChild();
        _current.removeChild(root);
        return root;
    }

} // JxtParser
